﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Kokomo.Mirror.CliMetadata;
using Kokomo.Mirror.CliMetadata.Tables;
using Kokomo.Mirror.CliMetadata.Signatures;
using System.IO;

namespace Kokomo.Mirror.CliMetadata
{
	public class MetadataContainer
	{

		public MetadataContainer(
			bool hasLargeStringHeap,
			byte[] stringData,
			bool hasLargeUserStringHeap,
			byte[] userStringData,
			bool hasLargeBlobHeap,
			byte[] blobData,
			bool hasLargeGuidHeap,
			byte[] guidData,
			MetadataOptions options
			)
		{
			if (stringData == null) throw new ArgumentNullException("stringData");
			if (blobData == null) throw new ArgumentNullException("blobData");
			if (guidData == null) throw new ArgumentNullException("guidData");

			this.HasLargeStringHeap = hasLargeStringHeap;
			this._stringData = stringData;

			this._userStringData = userStringData;

			this.HasLargeBlobHeap = hasLargeBlobHeap;
			this._blobData = blobData;

			this.HasLargeGuidHeap = hasLargeGuidHeap;
			this._guidData = guidData;

			this._stringTable = new Dictionary<int, string>();
			this._userStringTable = new Dictionary<int, string>();
			this._signatureTable = new Dictionary<int, Signature>();

			this.Options = options;

			this.InitializeTables();
			this.InitializeRelationships();
		}

		public MetadataOptions Options { get; private set; }
		public bool ResolvesInvalidStringsToNull { get { return this.Options.HasFlag(MetadataOptions.ResolveInvalidStringsToNull); } }
		public bool ResolvesInvalidUserStringsToNull { get { return this.Options.HasFlag(MetadataOptions.ResolveInvalidUserStringsToNull); } }
		public bool ResolvesInvalidBlobsToNull { get { return this.Options.HasFlag(MetadataOptions.ResolveInvalidBlobsToNull); } }

		private void InitializeTables()
		{
			this.Tables = new List<MetadataTable>();
			this.TablesByNumber = new Dictionary<MetadataTableNumber, MetadataTable>();

			this.AssemblyTable = this.GetTable<AssemblyRow>(MetadataTableNumber.Assembly);
			this.AssemblyOsTable = this.GetTable<AssemblyOsRow>(MetadataTableNumber.AssemblyOS);
			this.AssemblyProcessorTable = this.GetTable<AssemblyProcessorRow>(MetadataTableNumber.AssemblyProcessor);
			this.AssemblyRefTable = this.GetTable<AssemblyRefRow>(MetadataTableNumber.AssemblyRef);
			this.AssemblyRefOsTable = this.GetTable<AssemblyRefOsRow>(MetadataTableNumber.AssemblyRefOS);
			this.AssemblyRefProcessorTable = this.GetTable<AssemblyRefProcessorRow>(MetadataTableNumber.AssemblyRefProcessor);
			this.ClassLayoutTable = this.GetTable<ClassLayoutRow>(MetadataTableNumber.ClassLayout);
			this.ConstantTable = this.GetTable<ConstantRow>(MetadataTableNumber.Constant);
			this.CustomAttributeTable = this.GetTable<CustomAttributeRow>(MetadataTableNumber.CustomAttribute);
			this.DeclSecurityTable = this.GetTable<DeclSecurityRow>(MetadataTableNumber.DeclSecurity);
			this.EventMapTable = this.GetTable<EventMapRow>(MetadataTableNumber.EventMap);
			this.EventTable = this.GetTable<EventRow>(MetadataTableNumber.Event);
			this.ExportedTypeTable = this.GetTable<ExportedTypeRow>(MetadataTableNumber.ExportedType);
			this.FieldTable = this.GetTable<FieldRow>(MetadataTableNumber.Field);
			this.FieldLayoutTable = this.GetTable<FieldLayoutRow>(MetadataTableNumber.FieldLayout);
			this.FieldMarshalTable = this.GetTable<FieldMarshalRow>(MetadataTableNumber.FieldMarshal);
			this.FieldRvaTable = this.GetTable<FieldRvaRow>(MetadataTableNumber.FieldRva);
			this.FileTable = this.GetTable<FileRow>(MetadataTableNumber.File);
			this.GenericParamTable = this.GetTable<GenericParamRow>(MetadataTableNumber.GenericParam);
			this.GenericParamConstraintTable = this.GetTable<GenericParamConstraintRow>(MetadataTableNumber.GenericParamConstraint);
			this.ImplMapTable = this.GetTable<ImplMapRow>(MetadataTableNumber.ImplMap);
			this.InterfaceImplTable = this.GetTable<InterfaceImplRow>(MetadataTableNumber.InterfaceImpl);
			this.ManifestResourceTable = this.GetTable<ManifestResourceRow>(MetadataTableNumber.ManifestResource);
			this.MemberRefTable = this.GetTable<MemberRefRow>(MetadataTableNumber.MemberRef);
			this.MethodDefTable = this.GetTable<MethodDefRow>(MetadataTableNumber.MethodDef);
			this.MethodImplTable = this.GetTable<MethodImplRow>(MetadataTableNumber.MethodImpl);
			this.MethodSemanticsTable = this.GetTable<MethodSemanticsRow>(MetadataTableNumber.MethodSemantics);
			this.MethodSpecTable = this.GetTable<MethodSpecRow>(MetadataTableNumber.MethodSpec);
			this.ModuleTable = this.GetTable<ModuleRow>(MetadataTableNumber.Module);
			this.ModuleRefTable = this.GetTable<ModuleRefRow>(MetadataTableNumber.ModuleRef);
			this.NestedClassTable = this.GetTable<NestedClassRow>(MetadataTableNumber.NestedClass);
			this.ParamTable = this.GetTable<ParamRow>(MetadataTableNumber.Param);
			this.PropertyTable = this.GetTable<PropertyRow>(MetadataTableNumber.Property);
			this.PropertyMapTable = this.GetTable<PropertyMapRow>(MetadataTableNumber.PropertyMap);
			this.StandAloneSigTable = this.GetTable<StandAloneSigRow>(MetadataTableNumber.StandAloneSig);
			this.TypeDefTable = this.GetTable<TypeDefRow>(MetadataTableNumber.TypeDef);
			this.TypeRefTable = this.GetTable<TypeRefRow>(MetadataTableNumber.TypeRef);
			this.TypeSpecTable = this.GetTable<TypeSpecRow>(MetadataTableNumber.TypeSpec);
		}
		private void InitializeRelationships()
		{
			this.TypeDef_Field = new ParentChildRelationship<TypeDefRow, FieldRow>(
				this.TypeDefTable,
				this.FieldTable,
				r => r.FieldList.RowNumber
				);
			this.TypeDef_MethodDef = new ParentChildRelationship<TypeDefRow, MethodDefRow>(
				this.TypeDefTable,
				this.MethodDefTable,
				r => r.MethodList.RowNumber
				);
			this.EventMap_Event = new ParentChildRelationship<EventMapRow, EventRow>(
				this.EventMapTable,
				this.EventTable,
				r => r.EventList.RowNumber
				);
			this.PropertyMap_Property = new ParentChildRelationship<PropertyMapRow, PropertyRow>(
				this.PropertyMapTable,
				this.PropertyTable,
				r => r.PropertyList.RowNumber
				);
			this.MethodDef_Param = new ParentChildRelationship<MethodDefRow, ParamRow>(
				this.MethodDefTable,
				this.ParamTable,
				r => r.ParamList.RowNumber
				);
		}

		#region Heaps
		#region Strings
		public void PreloadStrings()
		{
			
		}

		private byte[] _stringData;
		private Dictionary<int, string> _stringTable;
		public bool HasLargeStringHeap { get; private set; }
		public string GetString(StringRef stringRef)
		{
			if (stringRef.HeapIndex < 0)
				return this._addedStrings[-stringRef.HeapIndex];

			return this.GetString(stringRef.HeapIndex);
		}
		protected string GetString(int initialHeapIndex)
		{
			if (initialHeapIndex == 0) return null;

			if (
				(initialHeapIndex <= 0)
				|| (initialHeapIndex >= this._stringData.Length)
				)
			{
				if (this.ResolvesInvalidStringsToNull)
					return null;
				else
					throw new ArgumentOutOfRangeException("initialHeapIndex");
			}

			string str;
			bool exists;
			//lock (this._stringTable)
				exists = this._stringTable.TryGetValue(initialHeapIndex, out str);

			if (!exists)
			{
				{
					int endIndex = initialHeapIndex;
					while (this._stringData[endIndex] != '\0') endIndex++;

					int length = endIndex - initialHeapIndex;
					str = Encoding.UTF8.GetString(this._stringData, initialHeapIndex, length);

					//StringBuilder builder = new StringBuilder();
					//int heapIndex = initialHeapIndex;
					//bool done = false;
					//while (!done)
					//{
					//    Decoder d = Encoding.UTF8.GetDecoder();

					//    char[] charArray = new char[1];
					//    int bytesUsed;
					//    int charsUsed;
					//    bool completed;

					//    do
					//    {
					//        d.Convert(
					//            this._stringData,
					//            heapIndex,
					//            1,
					//            charArray,
					//            0,
					//            1,
					//            false,
					//            out bytesUsed,
					//            out charsUsed,
					//            out completed
					//            );
					//        heapIndex += bytesUsed;
					//    } while (!completed);

					//    char c = charArray[0];
					//    if (c == '\0') break;
					//    builder.Append(c);
					//}

					//str = builder.ToString();
					lock (this._stringTable)
						this._stringTable[initialHeapIndex] = str;
				}
			}

			return str;
		}

		private List<string> _addedStrings = new List<string>();
		public StringRef CreateString(string str)
		{
			if (str == null) return new StringRef();

			int index = this._addedStrings.Count;
			this._addedStrings.Add(str);

			return new StringRef(-index);
		}

		public void CompactStrings()
		{
			StringHeapBuilder builder = new StringHeapBuilder(this, Encoding.UTF8);

			// AssemblyRef
			builder.UpdateRows(this.AssemblyRefTable, r => r.Name, (r, s) => r.Name = s);
			builder.UpdateRows(this.AssemblyRefTable, r => r.Culture, (r, s) => r.Culture = s);
			// Assembly
			builder.UpdateRows(this.AssemblyTable, r => r.Name, (r, s) => r.Name = s);
			builder.UpdateRows(this.AssemblyTable, r => r.Culture, (r, s) => r.Culture = s);
			// Event
			builder.UpdateRows(this.EventTable, r => r.Name, (r, s) => r.Name = s);
			// ExportedType
			builder.UpdateRows(this.ExportedTypeTable, r => r.TypeName, (r, s) => r.TypeName = s);
			builder.UpdateRows(this.ExportedTypeTable, r => r.TypeNamespace, (r, s) => r.TypeNamespace = s);
			// Field
			builder.UpdateRows(this.FieldTable, r => r.Name, (r, s) => r.Name = s);
			// File
			builder.UpdateRows(this.FileTable, r => r.Name, (r, s) => r.Name = s);
			// GenericParam
			builder.UpdateRows(this.GenericParamTable, r => r.Name, (r, s) => r.Name = s);
			// ImplMap
			builder.UpdateRows(this.ImplMapTable, r => r.ImportName, (r, s) => r.ImportName = s);
			// ManifestResource
			builder.UpdateRows(this.ManifestResourceTable, r => r.Name, (r, s) => r.Name = s);
			// MemberRef
			builder.UpdateRows(this.MemberRefTable, r => r.Name, (r, s) => r.Name = s);
			// MethodDef
			builder.UpdateRows(this.MethodDefTable, r => r.Name, (r, s) => r.Name = s);
			// ModuleRef
			builder.UpdateRows(this.ModuleRefTable, r => r.Name, (r, s) => r.Name = s);
			// Module
			builder.UpdateRows(this.ModuleTable, r => r.Name, (r, s) => r.Name = s);
			// Param
			builder.UpdateRows(this.ParamTable, r => r.Name, (r, s) => r.Name = s);
			// Property
			builder.UpdateRows(this.PropertyTable, r => r.Name, (r, s) => r.Name = s);
			// TypeDef
			builder.UpdateRows(this.TypeDefTable, r => r.TypeName, (r, s) => r.TypeName = s);
			builder.UpdateRows(this.TypeDefTable, r => r.TypeNamespace, (r, s) => r.TypeNamespace = s);
			// TypeRef
			builder.UpdateRows(this.TypeRefTable, r => r.TypeName, (r, s) => r.TypeName = s);
			builder.UpdateRows(this.TypeRefTable, r => r.TypeNamespace, (r, s) => r.TypeNamespace = s);

			this._addedStrings.Clear();
			this._stringData = builder.GetHeapData();
			this._stringTable.Clear();

			this.HasLargeStringHeap = this._stringData.Length > 0xFFFF;
		}
		#endregion
		#region User strings
		private byte[] _userStringData;
		private Dictionary<int, string> _userStringTable;
		public string GetUserString(int initialHeapIndex)
		{
			if (initialHeapIndex == 0) return null;

			if (
				(initialHeapIndex <= 0)
				|| (initialHeapIndex >= this._userStringData.Length)
				)
			{
				if (this.ResolvesInvalidUserStringsToNull)
					return null;
				else
					throw new ArgumentOutOfRangeException("initialHeapIndex");
			}

			string userString;
			lock (this._userStringTable)
			{
				if (this._userStringTable.TryGetValue(initialHeapIndex, out userString)) return userString;

				BitReader reader = new BitReader(this._userStringData)
				{
					Position = initialHeapIndex
				};

				//MemoryStream stream = new MemoryStream(this._userStringData);
				//stream.Position = initialHeapIndex;
				//BinaryReader reader = new BinaryReader(stream, Encoding.Unicode);
				int byteCount = reader.ReadCompressedInt();//SignatureReader.ReadCompressedInt(this._userStringData, ref initialHeapIndex);
				int stringStartIndex = reader.Position;
				//byte[] stringData = reader.ReadBytes(byteCount);
				userString = Encoding.Unicode.GetString(this._userStringData, stringStartIndex, byteCount - 1);

				this._userStringTable[initialHeapIndex] = userString;
			}

			return userString;
		}
		#endregion
		#region Signatures
		private Dictionary<int, Signature> _signatureTable;
		public Signature GetSignature(SignatureRef signatureRef)
		{
			return this.GetSignature(signatureRef.HeapIndex, signatureRef.SignatureKind);
		}
		public Signature GetSignature(int initialHeapIndex, SignatureKind kind)
		{
			Signature signature;
			lock (this)
			{
				if (!this._signatureTable.TryGetValue(initialHeapIndex, out signature))
				{
					byte[] data = this.GetBlob(initialHeapIndex);
					try
					{
						signature = SignatureReader.ReadSignature(kind, data);
					}
					catch { }
					//signature = SignatureReader.ReadSignature(this._blobData, initialHeapIndex);
					this._signatureTable[initialHeapIndex] = signature;
				}
			}
			return signature;
		}

		#endregion
		#region Blobs
		private byte[] _blobData;
		public byte[] GetBlob(BlobRef blobRef)
		{
			return this.GetBlob(blobRef.HeapIndex);
		}
		public byte[] GetBlob(SignatureRef signatureRef)
		{
			return this.GetBlob(signatureRef.HeapIndex);
		}
		protected byte[] GetBlob(int heapIndex)
		{
			if (heapIndex == 0) return null;
			byte[] blob = this.GetBlob(this._blobData, heapIndex);
			return blob;
		}

		protected byte[] GetBlob(
			byte[] heapData,
			int blobOffset
			)
		{
			if (
				(blobOffset <= 0)
				|| (blobOffset >= heapData.Length)
				)
			{
				if (this.ResolvesInvalidBlobsToNull)
					return null;
				else
					throw new ArgumentOutOfRangeException("blobOffset");
			}

			int blobLength = GetBlobLength(heapData, ref blobOffset);

			byte[] blob = new byte[blobLength];
			Array.Copy(heapData, blobOffset, blob, 0, blobLength);

			return blob;
		}
		protected int GetBlobLength(
			byte[] heapData,
			ref int blobOffset
			)
		{
			byte b1 = heapData[blobOffset++];
			int length;
			if ((b1 & 0x80) == 0)
			{
				// 0bbb bbbb
				length = b1;
			}

			else if ((b1 & 0xC0) == 0x80)
			{
				// 10bb bbbb
				length =
					((b1 & 0x3F) << 8)
					| heapData[blobOffset++]
					;
			}
			else if ((b1 & 0xE0) == 0xC0)
			{
				// 110b bbbb
				length =
					((b1 & 0x1F) << 24)
					| (heapData[blobOffset++] << 16)
					| (heapData[blobOffset++] << 8)
					| (heapData[blobOffset++])
					;
			}
			else
			{
				throw new ArgumentException("Unknown length encoding");
			}

			return length;
		}

		public bool HasLargeBlobHeap { get; private set; }
		#endregion
		#region GUIDs
		private byte[] _guidData;
		public bool HasLargeGuidHeap { get; private set; }
		public Guid GetGuid(int heapIndex)
		{
			if (heapIndex == 0) return new Guid();

			byte[] guidBytes = new byte[16];
			Array.Copy(this._guidData, (heapIndex - 1) * 16, guidBytes, 0, guidBytes.Length);

			Guid guid = new Guid(guidBytes);
			return guid;
		}
		#endregion
		#endregion

		public IMetadataRow GetRow(CodedIndex index)
		{
			if (index.RowNumber == 0) return null;

			MetadataTable table = this.GetTable(index.TableNumber);

			// Some tables contain indexes to a row just past the end of a table
			// to signify no rows (e.g., MethodDef.ParamList)
			if (index.RowNumber == (table.RowCount + 1))
				return null;

			IMetadataRow row = table.GetItem(index.RowNumber - 1);
			return row;
		}

		#region Tables

		public List<MetadataTable> Tables { get; private set; }
		public Dictionary<MetadataTableNumber, MetadataTable> TablesByNumber { get; private set; }

		public MetadataTable GetTable(MetadataTableNumber tableNumber)
		{
			return this.TablesByNumber[tableNumber];
		}
		protected MetadataTable<TRecord> GetTable<TRecord>(MetadataTableNumber tableNumber)
			where TRecord : IMetadataRow, new()
		{
			MetadataTable<TRecord> table = new MetadataTable<TRecord>(tableNumber, this);
			this.Tables.Add(table);
			this.TablesByNumber.Add(tableNumber, table);

			return table;
		}

		public int GetRowCount(MetadataTableNumber tableNumber)
		{
			return this.GetTable(tableNumber).RowCount;
		}

		public MetadataTable<AssemblyRow> AssemblyTable { get; private set; }
		public MetadataTable<AssemblyOsRow> AssemblyOsTable { get; private set; }
		public MetadataTable<AssemblyProcessorRow> AssemblyProcessorTable { get; private set; }
		public MetadataTable<AssemblyRefRow> AssemblyRefTable { get; private set; }
		public MetadataTable<AssemblyRefOsRow> AssemblyRefOsTable { get; private set; }
		public MetadataTable<AssemblyRefProcessorRow> AssemblyRefProcessorTable { get; private set; }
		public MetadataTable<ClassLayoutRow> ClassLayoutTable { get; private set; }
		public MetadataTable<ConstantRow> ConstantTable { get; private set; }
		public MetadataTable<CustomAttributeRow> CustomAttributeTable { get; private set; }
		public MetadataTable<DeclSecurityRow> DeclSecurityTable { get; private set; }
		public MetadataTable<EventMapRow> EventMapTable { get; private set; }
		public MetadataTable<EventRow> EventTable { get; private set; }
		public MetadataTable<ExportedTypeRow> ExportedTypeTable { get; private set; }
		public MetadataTable<FieldRow> FieldTable { get; private set; }
		public MetadataTable<FieldLayoutRow> FieldLayoutTable { get; private set; }
		public MetadataTable<FieldMarshalRow> FieldMarshalTable { get; private set; }
		public MetadataTable<FieldRvaRow> FieldRvaTable { get; private set; }
		public MetadataTable<FileRow> FileTable { get; private set; }
		public MetadataTable<GenericParamRow> GenericParamTable { get; private set; }
		public MetadataTable<GenericParamConstraintRow> GenericParamConstraintTable { get; private set; }
		public MetadataTable<ImplMapRow> ImplMapTable { get; private set; }
		public MetadataTable<InterfaceImplRow> InterfaceImplTable { get; private set; }
		public MetadataTable<ManifestResourceRow> ManifestResourceTable { get; private set; }
		public MetadataTable<MemberRefRow> MemberRefTable { get; private set; }
		public MetadataTable<MethodDefRow> MethodDefTable { get; private set; }
		public MetadataTable<MethodImplRow> MethodImplTable { get; private set; }
		public MetadataTable<MethodSemanticsRow> MethodSemanticsTable { get; private set; }
		public MetadataTable<MethodSpecRow> MethodSpecTable { get; private set; }
		public MetadataTable<ModuleRow> ModuleTable { get; private set; }
		public MetadataTable<ModuleRefRow> ModuleRefTable { get; private set; }
		public MetadataTable<NestedClassRow> NestedClassTable { get; private set; }
		public MetadataTable<ParamRow> ParamTable { get; private set; }
		public MetadataTable<PropertyRow> PropertyTable { get; private set; }
		public MetadataTable<PropertyMapRow> PropertyMapTable { get; private set; }
		public MetadataTable<StandAloneSigRow> StandAloneSigTable { get; private set; }
		public MetadataTable<TypeDefRow> TypeDefTable { get; private set; }
		public MetadataTable<TypeRefRow> TypeRefTable { get; private set; }
		public MetadataTable<TypeSpecRow> TypeSpecTable { get; private set; }
		#endregion

		#region Relationships
		public ParentChildRelationship<TypeDefRow, MethodDefRow> TypeDef_MethodDef { get; private set; }
		public ParentChildRelationship<TypeDefRow, FieldRow> TypeDef_Field { get; private set; }
		public ParentChildRelationship<EventMapRow, EventRow> EventMap_Event { get; private set; }
		public ParentChildRelationship<PropertyMapRow, PropertyRow> PropertyMap_Property { get; private set; }
		public ParentChildRelationship<MethodDefRow, ParamRow> MethodDef_Param { get; private set; }
		#endregion
	}
}
